1 module hip.ai.decisiontree;
2 version(Test):
3 
4 enum ResultTypes
5 {
6     _string,
7     _bool,
8     _int,
9     _uint,
10     _ushort,
11     _ulong,
12     _float,
13     _double,
14     _char,
15     _byte,
16     _ubyte,
17     _void,
18     _null,
19     _Condition
20 }
21 
22 struct ConditionResult
23 {
24     import hip.util.memory;
25     ResultTypes type;
26     void* value;
27 
28     T* get(T)(){return cast(T*)value;}
29     static ConditionResult create(T)(T val)
30     {
31         static if(is(T == void*))
32             return ConditionResult(ResultTypes._void, val);
33         else static if(__traits(compiles, mixin("ResultTypes._"~T.stringof)))
34             return ConditionResult(mixin("ResultTypes._"~T.stringof), toHeap(val));
35         else
36             static assert(false, "Could not create result of type "~T.stringof);
37     }
38 
39     static ConditionResult nullResult(){return ConditionResult(ResultTypes._null, null);}
40 
41     void dispose()
42     {
43         if(value != null)
44         {
45             free(value);
46             value = null;
47             type = ResultTypes._null;
48         }
49     }
50 }
51 
52 struct Condition
53 {
54     string name;
55     bool function(void* context) condition;
56     ConditionResult trueResult = ConditionResult.nullResult;
57     ConditionResult falseResult = ConditionResult.nullResult;
58 }
59 
60 
61 /**
62 
63 DecisionTree intuition:
64 
65 IsVisible:
66     true-> Returns PlayerMovement.run
67     false-> Returns a new branch if is hearing
68         IsHearing:
69             true-> Returns "They're hearing you!"
70             false-> Returns null
71 
72 Functions receive a void* context for accepting any data
73 You can set true or false results for branches by executing the following code
74 `tree.setTrueBranch("IsVisible.no.IsHearing", ConditionResult.create("They're hearing you!");`
75 
76 This will make the true branch result at isVisible(false branch) gets the value of the string.
77 
78 Basic example on how the decision tree works:
79 
80 HipDecisionTree playerAI = new HipDecisionTree("Enemy-based PlayerAI");
81     
82 playerAI.addCondition(Condition("IsVisible",
83 (void* ctx)
84 {
85     PlayerAIDecision* d = cast(PlayerAIDecision*)ctx;
86     writeln("Checking if visible");
87     return d.isVisible;
88 }));
89 playerAI.setTrueBranch("IsVisible", ConditionResult.create(cast(int)PlayerMovement.run))
90         .setFalseBranch("IsVisible", ConditionResult.create(Condition("IsHearing", 
91         (void* ctx)
92         {
93             PlayerAIDecision* d = cast(PlayerAIDecision*)ctx;
94             return d.isHearing;
95         })
96         ));
97 playerAI.setTrueBranch("IsVisible.no.IsHearing", ConditionResult.create("They're hearing you!"));
98 
99 PlayerAIDecision d;
100 d.isVisible = false;
101 d.isHearing = false;
102 ConditionResult res = playerAI.check("IsVisible", &d);
103 if(res.type == ResultTypes._int)
104 {
105     writeln(*cast(int*)res.value);
106 }
107 else if(res.type == ResultTypes._string)
108 {
109     writeln(*cast(string*)res.value);
110 }
111 */
112 
113 class HipDecisionTree
114 {
115     Condition[string] conditions;
116     import hip.util.string : split;
117     string name;
118     this(string name)
119     {
120         this.name = name;
121     }
122 
123     HipDecisionTree addCondition(Condition c)
124     {
125         conditions[c.name] = c;
126         return this;
127     }
128 
129     HipDecisionTree setTrueBranch(string conditionName, ConditionResult res)
130     {
131         getConditionByName(conditionName).trueResult = res;
132         return this;
133     }
134     HipDecisionTree setFalseBranch(string conditionName, ConditionResult res)
135     {
136         getConditionByName(conditionName).falseResult = res;
137         return this;
138     }
139 
140     ConditionResult check(string which, void* ctx)
141     {
142         assert((which in conditions) != null, "Could not find "~which);
143         Condition* c = which in conditions;
144         ConditionResult res;
145 
146         while(true)
147         {
148             bool isTrue = c.condition(ctx);
149             if(isTrue)
150                 res = c.trueResult;
151             else
152                 res = c.falseResult;
153             if(res.type == ResultTypes._Condition)
154                 c = cast(Condition*)res.value;
155             else
156                 return res;
157         }
158     }
159     protected Condition* getConditionByName(string name)
160     {
161         string[] names = name.split(".");
162         string lastName = names[0];
163         Condition* c = lastName in conditions;
164         if(names.length == 1)
165             return c;
166         assert(names.length % 2 != 0, "Names must not be divisible by 2. They must have a pre accessor as 'yes' or 'no'");
167 
168         bool isTrue;
169         for(int i = 1; i < names.length; i+= 2)
170         {
171             isTrue = names[i] == "yes";
172             if(isTrue)
173                 c = c.trueResult.get!Condition;
174             else
175                 c = c.falseResult.get!Condition;
176         }
177         return c;
178     }
179 
180 }